{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "initial_id",
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    ""
   ]
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-06-09T11:57:37.959359Z",
     "start_time": "2025-06-09T11:57:36.770232Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# %% [markdown]\n",
    "# # 信用风险模型构建\n",
    "# 完整流程：数据加载 → 特征工程 → 模型训练 → 预测提交\n",
    "\n",
    "# %% [code]\n",
    "# ======================\n",
    "# 初始化设置\n",
    "# ======================\n",
    "import sys\n",
    "from pathlib import Path\n",
    "import os\n",
    "import gc\n",
    "from glob import glob\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import polars as pl\n",
    "from datetime import datetime\n",
    "import seaborn as sns\n",
    "import matplotlib.pyplot as plt\n",
    "import joblib\n",
    "from sklearn.model_selection import StratifiedGroupKFold\n",
    "from sklearn.metrics import roc_auc_score\n",
    "import lightgbm as lgb\n",
    "from lightgbm import LGBMClassifier\n",
    "from sklearn.base import BaseEstimator, RegressorMixin\n",
    "from sklearn.preprocessing import LabelEncoder, OrdinalEncoder\n",
    "\n",
    "# 设置随机种子确保结果可复现\n",
    "def seed_it_all(seed=7):\n",
    "    os.environ['PYTHONHASHSEED'] = str(seed)\n",
    "    np.random.seed(seed)\n",
    "\n",
    "SEED = 0\n",
    "seed_it_all(SEED)\n",
    "print(\"环境设置完成! 随机种子:\", SEED)\n"
   ],
   "id": "df96fe615fb935ee",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "环境设置完成! 随机种子: 0\n"
     ]
    }
   ],
   "execution_count": 1
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-06-09T12:12:24.849814Z",
     "start_time": "2025-06-09T12:12:24.826787Z"
    }
   },
   "cell_type": "code",
   "source": [
    "plt.rcParams['font.sans-serif'] = ['SimHei']  # 指定默认字体为黑体\n",
    "plt.rcParams['axes.unicode_minus'] = False  # 正确显示负号\n",
    "import warnings\n",
    "warnings.filterwarnings('ignore')"
   ],
   "id": "5f9d5fb72993ccd4",
   "outputs": [],
   "execution_count": 14
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-06-09T11:58:14.277636Z",
     "start_time": "2025-06-09T11:58:14.259893Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# %% [code]\n",
    "# ======================\n",
    "# 数据处理管道\n",
    "# ======================\n",
    "class Pipeline:\n",
    "    @staticmethod\n",
    "    def set_table_dtypes(df):\n",
    "        \"\"\"设置正确的数据类型以节省内存\"\"\"\n",
    "        for col in df.columns:\n",
    "            if col in [\"case_id\", \"WEEK_NUM\", \"num_group1\", \"num_group2\"]:\n",
    "                df = df.with_columns(pl.col(col).cast(pl.Int32))\n",
    "            elif col in [\"date_decision\"]:\n",
    "                df = df.with_columns(pl.col(col).cast(pl.Date))\n",
    "            elif col[-1] in (\"P\", \"A\"):\n",
    "                df = df.with_columns(pl.col(col).cast(pl.Float32))\n",
    "            elif col[-1] in (\"M\",):\n",
    "                df = df.with_columns(pl.col(col).cast(pl.Utf8))\n",
    "            elif col[-1] in (\"D\",):\n",
    "                df = df.with_columns(pl.col(col).cast(pl.Date))\n",
    "        return df\n",
    "\n",
    "    @staticmethod\n",
    "    def handle_dates(df):\n",
    "        \"\"\"日期特征处理：转换为与决策日期的天数差\"\"\"\n",
    "        for col in df.columns:\n",
    "            if col[-1] in (\"D\",):\n",
    "                df = df.with_columns(pl.col(col) - pl.col(\"date_decision\"))\n",
    "                df = df.with_columns(pl.col(col).dt.total_days())\n",
    "        df = df.drop(\"date_decision\", \"MONTH\")\n",
    "        return df\n",
    "\n",
    "    @staticmethod\n",
    "    def filter_cols(df):\n",
    "        \"\"\"特征过滤：高缺失率和高基数特征\"\"\"\n",
    "        for col in df.columns:\n",
    "            if col not in [\"target\", \"case_id\", \"WEEK_NUM\"]:\n",
    "                isnull = df[col].is_null().mean()\n",
    "                if isnull > 0.6:\n",
    "                    df = df.drop(col)\n",
    "        \n",
    "        for col in df.columns:\n",
    "            if (col not in [\"target\", \"case_id\", \"WEEK_NUM\"]) & (df[col].dtype == pl.String):\n",
    "                freq = df[col].n_unique()\n",
    "                if (freq == 1) | (freq > 150):\n",
    "                    df = df.drop(col)\n",
    "        return df\n"
   ],
   "id": "2c8f6ebb52d2851c",
   "outputs": [],
   "execution_count": 2
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-06-09T11:58:16.682132Z",
     "start_time": "2025-06-09T11:58:16.660354Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# %% [code]\n",
    "# ======================\n",
    "# 特征聚合器\n",
    "# ======================\n",
    "class Aggregator:\n",
    "    @staticmethod\n",
    "    def num_expr(df):\n",
    "        \"\"\"数值型特征聚合表达式\"\"\"\n",
    "        cols = [col for col in df.columns if col[-1] in (\"P\", \"A\")]\n",
    "        expr_max = [pl.max(col).alias(f\"max_{col}\") for col in cols]\n",
    "        expr_last = [pl.last(col).alias(f\"last_{col}\") for col in cols]\n",
    "        expr_mean = [pl.mean(col).alias(f\"mean_{col}\") for col in cols]\n",
    "        return expr_max + expr_last + expr_mean\n",
    "    \n",
    "    @staticmethod\n",
    "    def date_expr(df):\n",
    "        \"\"\"日期特征聚合表达式\"\"\"\n",
    "        cols = [col for col in df.columns if col[-1] in (\"D\")]\n",
    "        expr_max = [pl.max(col).alias(f\"max_{col}\") for col in cols]\n",
    "        expr_last = [pl.last(col).alias(f\"last_{col}\") for col in cols]\n",
    "        expr_mean = [pl.mean(col).alias(f\"mean_{col}\") for col in cols]\n",
    "        return expr_max + expr_last + expr_mean\n",
    "    \n",
    "    @staticmethod\n",
    "    def str_expr(df):\n",
    "        \"\"\"字符串特征聚合表达式\"\"\"\n",
    "        cols = [col for col in df.columns if col[-1] in (\"M\",)]\n",
    "        expr_max = [pl.max(col).alias(f\"max_{col}\") for col in cols]\n",
    "        expr_last = [pl.last(col).alias(f\"last_{col}\") for col in cols]\n",
    "        return expr_max + expr_last\n",
    "    \n",
    "    @staticmethod\n",
    "    def other_expr(df):\n",
    "        \"\"\"其他特征聚合表达式\"\"\"\n",
    "        cols = [col for col in df.columns if col[-1] in (\"T\", \"L\")]\n",
    "        expr_max = [pl.max(col).alias(f\"max_{col}\") for col in cols]\n",
    "        expr_last = [pl.last(col).alias(f\"last_{col}\") for col in cols]\n",
    "        return expr_max + expr_last\n",
    "    \n",
    "    @staticmethod\n",
    "    def count_expr(df):\n",
    "        \"\"\"计数特征聚合表达式\"\"\"\n",
    "        cols = [col for col in df.columns if \"num_group\" in col]\n",
    "        expr_max = [pl.max(col).alias(f\"max_{col}\") for col in cols] \n",
    "        expr_last = [pl.last(col).alias(f\"last_{col}\") for col in cols]\n",
    "        return expr_max + expr_last\n",
    "    \n",
    "    @staticmethod\n",
    "    def get_exprs(df):\n",
    "        \"\"\"获取所有聚合表达式\"\"\"\n",
    "        exprs = Aggregator.num_expr(df) + \\\n",
    "                Aggregator.date_expr(df) + \\\n",
    "                Aggregator.str_expr(df) + \\\n",
    "                Aggregator.other_expr(df) + \\\n",
    "                Aggregator.count_expr(df)\n",
    "        return exprs"
   ],
   "id": "e4f8a7e575413dbf",
   "outputs": [],
   "execution_count": 3
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-06-09T11:58:21.380381Z",
     "start_time": "2025-06-09T11:58:21.368795Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# %% [code]\n",
    "# ======================\n",
    "# 数据读取函数\n",
    "# ======================\n",
    "def read_file(path, depth=None):\n",
    "    \"\"\"读取单个Parquet文件并进行处理\"\"\"\n",
    "    df = pl.read_parquet(path)\n",
    "    df = df.pipe(Pipeline.set_table_dtypes)\n",
    "    if depth in [1,2]:\n",
    "        df = df.group_by(\"case_id\").agg(Aggregator.get_exprs(df)) \n",
    "    return df\n",
    "\n",
    "def read_files(regex_path, depth=None):\n",
    "    \"\"\"读取多个Parquet文件并合并\"\"\"\n",
    "    chunks = []\n",
    "    for path in glob(str(regex_path)):\n",
    "        df = pl.read_parquet(path)\n",
    "        df = df.pipe(Pipeline.set_table_dtypes)\n",
    "        if depth in [1, 2]:\n",
    "            df = df.group_by(\"case_id\").agg(Aggregator.get_exprs(df))\n",
    "        chunks.append(df)\n",
    "    df = pl.concat(chunks, how=\"vertical_relaxed\")\n",
    "    df = df.unique(subset=[\"case_id\"])\n",
    "    return df"
   ],
   "id": "cd7749133a646b21",
   "outputs": [],
   "execution_count": 4
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-06-09T11:58:25.597271Z",
     "start_time": "2025-06-09T11:58:25.575359Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# %% [code]\n",
    "# ======================\n",
    "# 特征工程函数\n",
    "# ======================\n",
    "def feature_eng(df_base, depth_0, depth_1, depth_2):\n",
    "    \"\"\"主特征工程函数\"\"\"\n",
    "    df_base = (\n",
    "        df_base\n",
    "        .with_columns(\n",
    "            month_decision=pl.col(\"date_decision\").dt.month(),\n",
    "            weekday_decision=pl.col(\"date_decision\").dt.weekday(),\n",
    "        )\n",
    "    )\n",
    "    for i, df in enumerate(depth_0 + depth_1 + depth_2):\n",
    "        df_base = df_base.join(df, how=\"left\", on=\"case_id\", suffix=f\"_{i}\")\n",
    "    df_base = df_base.pipe(Pipeline.handle_dates)\n",
    "    return df_base\n",
    "\n",
    "def to_pandas(df_data, cat_cols=None):\n",
    "    \"\"\"将Polars DataFrame转换为Pandas并识别类别列\"\"\"\n",
    "    df_data = df_data.to_pandas()\n",
    "    if cat_cols is None:\n",
    "        cat_cols = list(df_data.select_dtypes(\"object\").columns)\n",
    "    return df_data, cat_cols\n",
    "\n",
    "def reduce_mem_usage(df):\n",
    "    \"\"\"减少Pandas DataFrame的内存使用\"\"\"\n",
    "    start_mem = df.memory_usage().sum() / 1024**2\n",
    "    print(f'初始内存使用: {start_mem:.2f} MB')\n",
    "    \n",
    "    for col in df.columns:\n",
    "        col_type = df[col].dtype\n",
    "        if col_type == 'category':\n",
    "            continue\n",
    "        \n",
    "        if col_type != object:\n",
    "            c_min = df[col].min()\n",
    "            c_max = df[col].max()\n",
    "            if str(col_type)[:3] == 'int':\n",
    "                if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:\n",
    "                    df[col] = df[col].astype(np.int8)\n",
    "                elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:\n",
    "                    df[col] = df[col].astype(np.int16)\n",
    "                elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:\n",
    "                    df[col] = df[col].astype(np.int32)\n",
    "                else:\n",
    "                    df[col] = df[col].astype(np.int64)  \n",
    "            else:\n",
    "                if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:\n",
    "                    df[col] = df[col].astype(np.float16)\n",
    "                elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:\n",
    "                    df[col] = df[col].astype(np.float32)\n",
    "                else:\n",
    "                    df[col] = df[col].astype(np.float64)\n",
    "        else:\n",
    "            if df[col].nunique() / len(df) < 0.5:\n",
    "                df[col] = df[col].astype(\"category\")\n",
    "    end_mem = df.memory_usage().sum() / 1024**2\n",
    "    print(f'优化后内存使用: {end_mem:.2f} MB')\n",
    "    print(f'内存减少: {(100 * (start_mem - end_mem) / start_mem):.1f}%')\n",
    "    return df"
   ],
   "id": "72c47ef4ddc14dfe",
   "outputs": [],
   "execution_count": 5
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-06-09T11:59:40.007553Z",
     "start_time": "2025-06-09T11:58:30.293069Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# %% [code]\n",
    "# ======================\n",
    "# 数据加载 - 训练集\n",
    "# ======================\n",
    "# 替换为你本地数据的实际路径\n",
    "ROOT = Path(\"E:/练习/信用风险模型/信用风险模型/数据工程和模型构建/kaggle/input/home-credit-credit-risk-model-stability\")\n",
    "TRAIN_DIR = ROOT / \"parquet_files\" / \"train\"\n",
    "\n",
    "print(\"开始加载训练数据...\")\n",
    "data_store = {\n",
    "    \"df_base\": read_file(TRAIN_DIR / \"train_base.parquet\"),\n",
    "    \"depth_0\": [\n",
    "        read_file(TRAIN_DIR / \"train_static_cb_0.parquet\"),\n",
    "        read_files(TRAIN_DIR / \"train_static_0_*.parquet\"),\n",
    "    ],\n",
    "    \"depth_1\": [\n",
    "        read_files(TRAIN_DIR / \"train_applprev_1_*.parquet\", 1),\n",
    "        read_file(TRAIN_DIR / \"train_tax_registry_a_1.parquet\", 1),\n",
    "        read_file(TRAIN_DIR / \"train_tax_registry_b_1.parquet\", 1),\n",
    "        read_file(TRAIN_DIR / \"train_tax_registry_c_1.parquet\", 1),\n",
    "        read_files(TRAIN_DIR / \"train_credit_bureau_a_1_*.parquet\", 1),\n",
    "        read_file(TRAIN_DIR / \"train_credit_bureau_b_1.parquet\", 1),\n",
    "        read_file(TRAIN_DIR / \"train_other_1.parquet\", 1),\n",
    "        read_file(TRAIN_DIR / \"train_person_1.parquet\", 1),\n",
    "        read_file(TRAIN_DIR / \"train_deposit_1.parquet\", 1),\n",
    "        read_file(TRAIN_DIR / \"train_debitcard_1.parquet\", 1),\n",
    "    ],\n",
    "    \"depth_2\": [\n",
    "        read_file(TRAIN_DIR / \"train_credit_bureau_b_2.parquet\", 2),\n",
    "        read_files(TRAIN_DIR / \"train_credit_bureau_a_2_*.parquet\", 2),\n",
    "        read_file(TRAIN_DIR / \"train_applprev_2.parquet\", 2),\n",
    "        read_file(TRAIN_DIR / \"train_person_2.parquet\", 2)\n",
    "    ]\n",
    "}\n",
    "\n",
    "df_train = feature_eng(**data_store)\n",
    "print(f\"训练集初始形状: {df_train.shape}\")\n",
    "\n",
    "# 数据抽样：只保留50%的数据\n",
    "sample_ratio = 0.5\n",
    "df_train = df_train.sample(fraction=sample_ratio, seed=SEED)\n",
    "print(f\"抽样后训练集形状: {df_train.shape}\")\n",
    "\n",
    "del data_store\n",
    "gc.collect()"
   ],
   "id": "32f4d500225c33f1",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "开始加载训练数据...\n",
      "训练集初始形状: (1526659, 861)\n",
      "抽样后训练集形状: (763329, 861)\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "5"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 6
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-06-09T12:01:07.273364Z",
     "start_time": "2025-06-09T12:00:05.386609Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# %% [code]\n",
    "# ======================\n",
    "# 特征选择与过滤\n",
    "# ======================\n",
    "df_train = df_train.pipe(Pipeline.filter_cols)\n",
    "df_train, cat_cols = to_pandas(df_train)\n",
    "df_train = reduce_mem_usage(df_train)\n",
    "print(f\"过滤后训练集形状: {df_train.shape}\")\n",
    "\n",
    "# 特征选择（基于缺失值分组和相关性）\n",
    "nums = df_train.select_dtypes(exclude='category').columns\n",
    "nans_df = df_train[nums].isna()\n",
    "nans_groups = {}\n",
    "for col in nums:\n",
    "    cur_group = nans_df[col].sum()\n",
    "    try:\n",
    "        nans_groups[cur_group].append(col)\n",
    "    except:\n",
    "        nans_groups[cur_group] = [col]\n",
    "del nans_df; gc.collect()\n",
    "\n",
    "def reduce_group(grps):\n",
    "    \"\"\"减少相关特征组，保留最具代表性的特征\"\"\"\n",
    "    use = []\n",
    "    for g in grps:\n",
    "        mx = 0; vx = g[0]\n",
    "        for gg in g:\n",
    "            n = df_train[gg].nunique()\n",
    "            if n > mx:\n",
    "                mx = n\n",
    "                vx = gg\n",
    "        use.append(vx)\n",
    "    return use\n",
    "\n",
    "def group_columns_by_correlation(matrix, threshold=0.85):\n",
    "    \"\"\"基于相关性分组特征\"\"\"\n",
    "    correlation_matrix = matrix.corr()\n",
    "    groups = []\n",
    "    remaining_cols = list(matrix.columns)\n",
    "    while remaining_cols:\n",
    "        col = remaining_cols.pop(0)\n",
    "        group = [col]\n",
    "        correlated_cols = [col]\n",
    "        for c in remaining_cols:\n",
    "            if correlation_matrix.loc[col, c] >= threshold:\n",
    "                group.append(c)\n",
    "                correlated_cols.append(c)\n",
    "        groups.append(group)\n",
    "        remaining_cols = [c for c in remaining_cols if c not in correlated_cols]\n",
    "    return groups\n",
    "\n",
    "uses = []\n",
    "for k, v in nans_groups.items():\n",
    "    if len(v) > 1:\n",
    "        Vs = nans_groups[k]\n",
    "        grps = group_columns_by_correlation(df_train[Vs], threshold=0.85)\n",
    "        use = reduce_group(grps)\n",
    "        uses += use\n",
    "    else:\n",
    "        uses += v\n",
    "uses += list(df_train.select_dtypes(include='category').columns)\n",
    "df_train = df_train[uses]\n",
    "\n",
    "# 保存训练集特征列\n",
    "feature_columns = df_train.columns.tolist()\n",
    "print(f\"最终训练特征数量: {len(feature_columns)}\")"
   ],
   "id": "68471acd0340d772",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "初始内存使用: 2122.02 MB\n",
      "优化后内存使用: 693.10 MB\n",
      "内存减少: 67.3%\n",
      "过滤后训练集形状: (763329, 431)\n",
      "最终训练特征数量: 363\n"
     ]
    }
   ],
   "execution_count": 7
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-06-09T12:01:43.959084Z",
     "start_time": "2025-06-09T12:01:40.968025Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# %% [code]\n",
    "# ======================\n",
    "# 数据加载 - 测试集\n",
    "# ======================\n",
    "TEST_DIR = ROOT / \"parquet_files\" / \"test\"\n",
    "sample = pd.read_csv(ROOT / \"sample_submission.csv\")\n",
    "\n",
    "print(\"\\n开始加载测试数据...\")\n",
    "data_store = {\n",
    "    \"df_base\": read_file(TEST_DIR / \"test_base.parquet\"),\n",
    "    \"depth_0\": [\n",
    "        read_file(TEST_DIR / \"test_static_cb_0.parquet\"),\n",
    "        read_files(TEST_DIR / \"test_static_0_*.parquet\"),\n",
    "    ],\n",
    "    \"depth_1\": [\n",
    "        read_files(TEST_DIR / \"test_applprev_1_*.parquet\", 1),\n",
    "        read_file(TEST_DIR / \"test_tax_registry_a_1.parquet\", 1),\n",
    "        read_file(TEST_DIR / \"test_tax_registry_b_1.parquet\", 1),\n",
    "        read_file(TEST_DIR / \"test_tax_registry_c_1.parquet\", 1),\n",
    "        read_files(TEST_DIR / \"test_credit_bureau_a_1_*.parquet\", 1),\n",
    "        read_file(TEST_DIR / \"test_credit_bureau_b_1.parquet\", 1),\n",
    "        read_file(TEST_DIR / \"test_other_1.parquet\", 1),\n",
    "        read_file(TEST_DIR / \"test_person_1.parquet\", 1),\n",
    "        read_file(TEST_DIR / \"test_deposit_1.parquet\", 1),\n",
    "        read_file(TEST_DIR / \"test_debitcard_1.parquet\", 1),\n",
    "    ],\n",
    "    \"depth_2\": [\n",
    "        read_file(TEST_DIR / \"test_credit_bureau_b_2.parquet\", 2),\n",
    "        read_files(TEST_DIR / \"test_credit_bureau_a_2_*.parquet\", 2),\n",
    "        read_file(TEST_DIR / \"test_applprev_2.parquet\", 2),\n",
    "        read_file(TEST_DIR / \"test_person_2.parquet\", 2)\n",
    "    ]\n",
    "}\n",
    "\n",
    "df_test = feature_eng(**data_store)\n",
    "print(f\"测试集初始形状: {df_test.shape}\")\n",
    "del data_store\n",
    "gc.collect()\n",
    "\n",
    "# 确保测试集特征与训练集一致\n",
    "train_features = [col for col in feature_columns if col != \"target\"]\n",
    "missing_cols = set(train_features) - set(df_test.columns)\n",
    "if missing_cols:\n",
    "    print(f\"添加缺失列: {missing_cols}\")\n",
    "    for col in missing_cols:\n",
    "        df_test = df_test.with_columns(pl.lit(None).alias(col))\n",
    "\n",
    "df_test = df_test.select(train_features)\n",
    "print(f\"对齐后测试集形状: {df_test.shape}\")\n",
    "\n",
    "df_test, _ = to_pandas(df_test)\n",
    "df_test = reduce_mem_usage(df_test)\n",
    "gc.collect()"
   ],
   "id": "7770eb474b47960f",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "开始加载测试数据...\n",
      "测试集初始形状: (10, 860)\n",
      "对齐后测试集形状: (10, 362)\n",
      "初始内存使用: 0.02 MB\n",
      "优化后内存使用: 0.02 MB\n",
      "内存减少: 19.9%\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "0"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 8
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": "",
   "id": "4a868b50f7d3a52d"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-06-09T12:02:55.441441Z",
     "start_time": "2025-06-09T12:02:08.682776Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# %% [code]\n",
    "# ======================\n",
    "# 类别特征编码\n",
    "# ======================\n",
    "# 准备训练数据\n",
    "y = df_train[\"target\"]\n",
    "weeks = df_train[\"WEEK_NUM\"]\n",
    "X = df_train.drop(columns=[\"target\", \"case_id\", \"WEEK_NUM\"])\n",
    "\n",
    "# 识别类别特征\n",
    "categorical_cols = list(X.select_dtypes(include=['category', 'object']).columns)\n",
    "print(f\"发现 {len(categorical_cols)} 个类别特征\")\n",
    "\n",
    "# 初始化编码器\n",
    "encoder = OrdinalEncoder(handle_unknown='use_encoded_value', unknown_value=-1)\n",
    "\n",
    "# 训练集编码\n",
    "if categorical_cols:\n",
    "    X[categorical_cols] = encoder.fit_transform(X[categorical_cols].astype(str))\n",
    "    df_test[categorical_cols] = encoder.transform(df_test[categorical_cols].astype(str))\n",
    "\n",
    "# 转换数据类型\n",
    "X = X.astype(np.float32)\n",
    "df_test = df_test.astype(np.float32)"
   ],
   "id": "a1e965a0829e8448",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "发现 107 个类别特征\n"
     ]
    }
   ],
   "execution_count": 9
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-06-09T12:06:47.048372Z",
     "start_time": "2025-06-09T12:03:16.229995Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# %% [code]\n",
    "# ======================\n",
    "# 模型训练\n",
    "# ======================\n",
    "device = 'cpu'\n",
    "n_est = 300\n",
    "\n",
    "# LightGBM参数\n",
    "params_lgb = {\n",
    "    \"boosting_type\": \"gbdt\",\n",
    "    \"objective\": \"binary\",\n",
    "    \"metric\": \"auc\",\n",
    "    \"max_depth\": 8,\n",
    "    \"learning_rate\": 0.06,\n",
    "    \"n_estimators\": n_est,\n",
    "    \"colsample_bytree\": 0.7,\n",
    "    \"colsample_bynode\": 0.7,\n",
    "    \"subsample\": 0.8,\n",
    "    \"subsample_freq\": 1,\n",
    "    \"verbose\": -1,\n",
    "    \"random_state\": SEED,\n",
    "    \"reg_alpha\": 0.2,\n",
    "    \"reg_lambda\": 15,\n",
    "    \"extra_trees\": True,\n",
    "    'num_leaves': 48,\n",
    "    \"device\": device, \n",
    "}\n",
    "\n",
    "# 交叉验证\n",
    "fitted_models_lgb = []\n",
    "cv_scores_lgb = []\n",
    "oof_preds = np.zeros(len(X))\n",
    "\n",
    "# 使用StratifiedGroupKFold\n",
    "cv = StratifiedGroupKFold(n_splits=3, shuffle=True, random_state=SEED)\n",
    "\n",
    "print(\"\\n开始交叉验证训练...\")\n",
    "for fold, (idx_train, idx_valid) in enumerate(cv.split(X, y, groups=weeks)):\n",
    "    print(f\"\\n======= Fold {fold+1} =======\")\n",
    "    \n",
    "    X_train, X_valid = X.iloc[idx_train], X.iloc[idx_valid]\n",
    "    y_train, y_valid = y.iloc[idx_train], y.iloc[idx_valid]\n",
    "    \n",
    "    # 内存优化\n",
    "    X_train = X_train.astype(np.float32)\n",
    "    X_valid = X_valid.astype(np.float32)\n",
    "    \n",
    "    clf_lgb = LGBMClassifier(**params_lgb)\n",
    "    clf_lgb.fit(\n",
    "        X_train, y_train,\n",
    "        eval_set=[(X_valid, y_valid)],\n",
    "        callbacks=[lgb.log_evaluation(100), lgb.early_stopping(50, verbose=False)]\n",
    "    )\n",
    "    \n",
    "    fitted_models_lgb.append(clf_lgb)\n",
    "    y_pred_valid = clf_lgb.predict_proba(X_valid)[:, 1]\n",
    "    auc_score = roc_auc_score(y_valid, y_pred_valid)\n",
    "    cv_scores_lgb.append(auc_score)\n",
    "    oof_preds[idx_valid] = y_pred_valid\n",
    "    print(f\"Fold {fold+1} AUC: {auc_score:.4f}\")\n",
    "    \n",
    "    # 释放内存\n",
    "    del X_train, X_valid, y_train, y_valid\n",
    "    gc.collect()\n",
    "\n",
    "print(\"\\n交叉验证结果:\")\n",
    "print(f\"LightGBM CV AUC scores: {cv_scores_lgb}\")\n",
    "print(f\"Mean CV AUC: {np.mean(cv_scores_lgb):.4f}\")\n"
   ],
   "id": "e1531762289fa8e1",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "开始交叉验证训练...\n",
      "\n",
      "======= Fold 1 =======\n",
      "[100]\tvalid_0's auc: 0.832792\n",
      "[200]\tvalid_0's auc: 0.839648\n",
      "[300]\tvalid_0's auc: 0.842075\n",
      "Fold 1 AUC: 0.8421\n",
      "\n",
      "======= Fold 2 =======\n",
      "[100]\tvalid_0's auc: 0.835456\n",
      "[200]\tvalid_0's auc: 0.843519\n",
      "[300]\tvalid_0's auc: 0.846107\n",
      "Fold 2 AUC: 0.8461\n",
      "\n",
      "======= Fold 3 =======\n",
      "[100]\tvalid_0's auc: 0.833598\n",
      "[200]\tvalid_0's auc: 0.840961\n",
      "[300]\tvalid_0's auc: 0.843917\n",
      "Fold 3 AUC: 0.8439\n",
      "\n",
      "交叉验证结果:\n",
      "LightGBM CV AUC scores: [0.8420749679179362, 0.8461074115801223, 0.8439220690162843]\n",
      "Mean CV AUC: 0.8440\n"
     ]
    }
   ],
   "execution_count": 10
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-06-09T12:06:58.259444Z",
     "start_time": "2025-06-09T12:06:58.247138Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# %% [code]\n",
    "# ======================\n",
    "# 模型集成\n",
    "# ======================\n",
    "class VotingModel(BaseEstimator, RegressorMixin):\n",
    "    \"\"\"简单集成模型\"\"\"\n",
    "    def __init__(self, estimators):\n",
    "        super().__init__()\n",
    "        self.estimators = estimators\n",
    "        \n",
    "    def fit(self, X, y=None):\n",
    "        return self\n",
    "    \n",
    "    def predict(self, X):\n",
    "        y_preds = [estimator.predict(X) for estimator in self.estimators]\n",
    "        return np.mean(y_preds, axis=0)\n",
    "    \n",
    "    def predict_proba(self, X):\n",
    "        y_preds = [estimator.predict_proba(X) for estimator in self.estimators]\n",
    "        return np.mean(y_preds, axis=0)\n",
    "\n",
    "model = VotingModel(fitted_models_lgb)\n",
    "print(\"模型集成完成!\")"
   ],
   "id": "24ee27fe2a58a224",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "模型集成完成!\n"
     ]
    }
   ],
   "execution_count": 11
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-06-09T12:07:04.046353Z",
     "start_time": "2025-06-09T12:07:03.964178Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# %% [code]\n",
    "# ======================\n",
    "# 测试集预测\n",
    "# ======================\n",
    "model_features = [col for col in train_features if col not in [\"case_id\", \"WEEK_NUM\"]]\n",
    "df_test_model = df_test[model_features]\n",
    "print(f\"测试集预测特征数量: {len(df_test_model.columns)}\")\n",
    "\n",
    "# 分批预测\n",
    "test_preds = np.zeros(len(df_test_model))\n",
    "batch_size = 50000\n",
    "num_batches = (len(df_test_model) // batch_size) + 1\n",
    "\n",
    "print(\"\\n开始测试集预测...\")\n",
    "for i in range(num_batches):\n",
    "    start_idx = i * batch_size\n",
    "    end_idx = min((i + 1) * batch_size, len(df_test_model))\n",
    "    \n",
    "    if start_idx < end_idx:\n",
    "        batch = df_test_model.iloc[start_idx:end_idx]\n",
    "        batch_preds = model.predict_proba(batch)[:, 1]\n",
    "        test_preds[start_idx:end_idx] = batch_preds\n",
    "        print(f\"处理批次 {i+1}/{num_batches} - {end_idx-start_idx} 条样本\")\n"
   ],
   "id": "b7401b44d36197ff",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "测试集预测特征数量: 360\n",
      "\n",
      "开始测试集预测...\n",
      "处理批次 1/1 - 10 条样本\n"
     ]
    }
   ],
   "execution_count": 12
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-06-09T12:12:33.512232Z",
     "start_time": "2025-06-09T12:12:31.932836Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# %% [code]\n",
    "# ======================\n",
    "# 结果后处理与提交\n",
    "# ======================\n",
    "df_subm = sample.copy()\n",
    "df_subm['score'] = test_preds\n",
    "\n",
    "# 后处理：低分校正\n",
    "threshold = 0.995\n",
    "correction = 0.04\n",
    "low_score_mask = df_subm['score'] < threshold\n",
    "df_subm.loc[low_score_mask, 'score'] = (df_subm.loc[low_score_mask, 'score'] * threshold - correction).clip(0)\n",
    "\n",
    "# 保存结果\n",
    "df_subm.to_csv(\"submission.csv\", index=False)\n",
    "print(f\"\\n提交文件保存完成! 形状: {df_subm.shape}\")\n",
    "print(\"最终得分范围:\", df_subm['score'].min(), \"-\", df_subm['score'].max())\n",
    "\n",
    "# 可视化预测分布\n",
    "plt.figure(figsize=(10, 6))\n",
    "sns.histplot(df_subm['score'], bins=50, kde=True)\n",
    "plt.title('预测分数分布')\n",
    "plt.xlabel('预测分数')\n",
    "plt.ylabel('频数')\n",
    "plt.savefig('score_distribution.png', dpi=300)\n",
    "plt.show()\n",
    "\n",
    "print(\"流程完成!\")"
   ],
   "id": "d58ba87c36d0fa73",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "提交文件保存完成! 形状: (10, 2)\n",
      "最终得分范围: 0.0 - 0.06844508061642363\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "<Figure size 1000x600 with 1 Axes>"
      ],
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAz0AAAIgCAYAAACvVweEAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAASKZJREFUeJzt3Xl8FPX9x/H37JHNxRFAggIiKoeiXELBIlQUqQIqKnjVA/GnUrVYxZbigVoURS3FopYCYr0QETGKiC1Yq4jgjYCCaAooCAmQALmzx/z+SLJkIcAGdrPjN6/ngzyyOzPfmc/sZ5Psm5mdtWzbtgUAAAAAhnIlugAAAAAAiCdCDwAAAACjEXoAAAAAGI3QAwAAAMBohB4AAAAARiP0AAAAADAaoQcAAACA0Qg9AAAAAIxG6AEAAABgNEIPAKBOlJWVadeuXQecHwqF5Pf7w/fz8vL05ZdfRr3+tWvXatWqVftNz87OrlWd5eXlEbc//PDDWo2v8uCDD2rhwoWHNRYAEFuEHgBAhOLiYu3Zs0elpaUqKyuL+CooKFBRUZGCwaBuuukmvfTSS5KkXbt2af369ft9BQKB8HrffPNNDRky5IDbXbRokYYMGaKdO3dKkubNm6dRo0apqKjogGPef/99jRo1Sn6/Xy+//LKeffZZSdLw4cP12WefKRQK6eKLL9aLL74Y1b5v3bpV/fv31/LlyyVJ33zzja6//vpahS9JysnJ0YsvvqjNmzfXahwAID48iS4AAJAYV199tdatWyev1yvLsiRJ3bp1U8eOHTVt2jQlJSXJ7/crGAwqNTVVUsXRmssuu0z33HOPTj/9dD388MP6+uuv1bVrV02YMEFHHXWUJCkQCCgnJ0cff/yxGjduLEnyer1KSkqqsRbbtjVt2jQdc8wxatq0qSTpN7/5jZ5//nlNnTpVf/rTn2oc16tXLz355JOaNWuWkpKS5PV69f777ys3N1edO3dWdna2SktL1b9//6gek+nTpysQCOiUU06RJHXt2lX9+/fXxIkT9fLLL8vj2f/PZmFhodxut5KSkuR2uyVJs2fPVnp6uq666qr9lg+FQgoEAvL7/UpLS4uqLgDAkSH0AEA99fjjj8u2bSUlJemTTz7RuHHjdOmll6pfv3669dZbJUmTJk3Spk2b9PTTT+83fsSIETr11FNVUlKiHTt2qGvXrnrhhRckSZs3b9bZZ58tr9er4cOH69xzz1WzZs0OWMubb76p77//XpMmTQpPS0tL05133qk//elP+tWvfqXTTz99v3FLly7VoEGDlJGRoS+//FJFRUXKzs7W2WefrY8//lgbNmxQu3bt1LJly/CYsrIyeTyecECpsmnTJs2bN0933nmnGjRoEJ4+duxYXXjhhXr66ac1evTo/Wq44IILtGXLlhr3q3v37gfc5y5dumju3LkHnA8AiB1CDwDUU5mZmZo2bZqSk5M1Y8YMPfbYY+rXr1/EMhs2bFCnTp1qHG/btk477TRJ0htvvCGp4v0zkydP1g033BBezufzHfAIjyTl5uZq4sSJuvjii3XyySdHzBs6dKj+/e9/a/To0Zo1a5ZOPfXUiPkLFy7Uzp07tXXrVv34449q3ry5li1bpmAwKJfLpU2bNum7775Thw4dIsa99NJL6tGjR/h+KBTS2LFj1apVK1155ZURy7Zt21Z33HGHHnnkEbVo0UKXXnppxPxnn31WlmWFj/Q89thj+vjjjzV79mxt2bJF2dnZGjBgwH777fV6D/iYAABii/f0AEA91rt3b02fPl3HHXdcxAvzU045RaeccoqWLVum6dOn69RTTw1PkyqOsAwfPny/Ixy2bWvJkiUR0yzLCp8+t69gMKhx48YpNTVVY8eOrXGZRx99VCeccIKuueYaZWVlRcybMmWK7rvvPhUXF+u4445Tjx49dPzxx+vJJ5/UzTffrI8++khPP/20vvjiCy1evFiS9Pbbb6tz584R6/n73/+uNWvWaNKkSTWGkREjRug3v/mN7r33Xk2aNCniYgdt2rTRscceqxYtWmjr1q1asGCB7r33XrVs2VLfffedZsyYoaOOOmq/r6rT/gAA8UfoAYB6yLZtlZSUqEuXLnrppZfUtWtXhUKh8Hyfz6dnnnlGK1eu1EsvvaTly5frmWeekc/nk1QRiho2bKjhw4frm2++CY+res9LNO9VsW1b48eP1yeffKLbb79de/bs0bZt2/b7Kiws1MMPP6zu3btr7Nix+r//+z/l5eXJtm298cYbuvLKK3XjjTeqX79+crlc2r59u37729/qtddeUyAQkG3bSktLU2FhoSzLUuvWrSOOPL3++uuaOnWqrrnmGmVkZGjr1q011nHDDTdo5MiRevbZZ3XhhRfud6W4HTt26LbbbtOwYcPCAdLj8XBEBwAcgNPbAKAe2rFjh8444wy5XC7Zti2p4jStYDCoRx55JBxe9uzZo+HDh2v27NmS9oaajIwM/eMf/9DDDz+szMxMfffddxHrrwpHBzNnzhzNnz9fjz32mJ588klt2LDhoMvPmjVLvXv31tq1a5WRkaHc3Fy9+uqrmjhxonr16qUZM2aoU6dOuvLKKzVv3jxNnz5dDRs21Lp16zRgwADl5uYqMzMzIvCsX79ed999ty6//HIVFhbWeBpadaNGjdL06dP15JNPqm3btuHpu3fv1k033aSffvpJc+fO3e+9OvueXvfggw9q+PDhh3yMAACxQegBgHqoWbNmWr16tZKSkvTQQw+puLhYv//973XGGWfoF7/4RXi5srIySdJxxx2n77//PmIdXq9X48aNk8fjCQenvLw8paamyuWqOJGganpNLr/88vBpc3369JHH49H06dO1aNGi8HuEpIrLRl911VVq1aqV+vTpE57evHlzPf300/J6vXrrrbc0Z84cLVq0SB6PR3369NG7776rX/3qV/rPf/4jSdq4caNOPPHEiBrat2+vuXPnqlOnTioqKtKYMWP05Zdf6qabbtI777yj5s2bh5ft06ePWrVqpX79+kW89yknJ0c33nij9uzZo1NOOUWdOnXSzTffLEl666239Morr4Qv8CBJw4YN4+gPANQxQg8A1ENVb7yvbufOnRo6dGjElc4KCgrk8XjUpEmTGteTlZWld955RwMHDpRUEQAyMzPD86u/96WmGqreI5SRkSGp4gpq7du3jzg9bvfu3bIsS0cffXTE+IKCAvXs2TNi2i9/+cvw7XfffVeWZenxxx9Xfn6+vvjiC3Xr1m2/OqpqSE9Pl1Rx5bmGDRtGHMkpKChQSUnJfjVs2bJFw4cPV3Jysp577jk98sgjSktLU4sWLSRJjRo1ktvtDt+XJJfLRegBgDrGe3oAAJKkjh07hi8ZXXWE5vvvv1fr1q3DFyKo/mGjUsVV0Bo1aqR27drpvPPO02effaZjjjlGjRo10m233Safz6dgMCi/33/I7RcXF2v58uXq1atXxPScnBw1adJkv5DWoEEDrVixQq+99ppSUlK0ZMkSrVq1Sr/73e/UtWtXtWrVSi1btlTnzp31zDPPaOnSpRGh6EDefffdiKNdkrRt2zZJ2i/0tGzZUn/84x/18ssv69hjjz3k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N9Y111yj7du3a/PmzZoyZYo2btyoQYMG6fXXX9djjz2m9u3bx6NeAAAAAKiVqEPPa6+9pszMTPXs2VPJycnq27dveN7q1av16KOPas6cOerWrZsGDBgQl2IBAAAAoLaifk/P119/rUcffVQ9e/bUjh079MQTT2jMmDE666yz9NBDD+mCCy7QihUr1KhRI02ePDmeNQMAAABA1KI+0lN18YL169crKytLq1at0ooVK/R///d/uv3228PLTZgwQRdccIEGDx6sDh06xL5iAAAAAKiFqI/0PPLIIxo3bpw++OAD7dmzR5MmTdKFF16oNm3aaNSoUXrnnXdUXFysa6+9VqNGjVJOTk486wYAAACAqEQdeu644w717t1bxcXFcrlcGjNmjPr166eLL75YK1euVFZWls455xyddNJJuuyyy9SvX7941g0AAAAAUYn69LbRo0fL6/UqLy9PO3bsUNu2bfXvf/9b3bp1U6NGjTRt2jSNHj1ay5Yt0/bt23XUUUfFs24AAAAAiErUR3rOPfdcnXfeeerRo4c2bdqk5ORkDRw4UFdccYV27dqlL774Qj/88IP+8Ic/aOLEifGsGQAAAACiFnXoOe200/TMM88oLS1NAwYM0E8//aTk5GQtXLhQkvToo4+qRYsWGjx4sH744Qdt3LgxXjUDAAAAQNSiPr1t+fLluvHGG2VZlv73v//p3nvv1RVXXKG5c+cqOTlZc+bMUW5uriRp8ODB+vLLL3XcccfFq24AAAAAiErUoefss89W06ZN5ff71bdvX6WkpOgf//iHmjZtqilTpkiSmjdvLklq27at+vfvH5eCAQAAAKA2ogo9ZWVluvbaa/XWW2/J6/VqwoQJWrt2rdxutyTJtm35/X7dcsst8vl8uuuuu/TKK6/o2GOPjWvxAAAAAHAoUYUen88XDjiStH37dt133337LZeamqoRI0Zo6tSpBB4AAAAAjhD16W0lJSXKzs5Wq1at5Ha7dcopp2j8+PFyu91q2rSpWrdurd69e+vxxx9Xjx494lkzAAAAAEQt6qu37dixQ3fddZfOOeccffrpp5KkVq1aKTMzU5L0wQcfaOjQofr666/jUykAAAAAHIaoQ0/r1q31yiuv6IMPPlDXrl01Y8YMWZYlt9utpKQkdezYUf/4xz/09ttv65///GfUBVx//fWaP3/+4dQOAAAAAIcU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"
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "流程完成!\n"
     ]
    }
   ],
   "execution_count": 15
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": "",
   "id": "da3aeb1fe4654133"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": "",
   "id": "8b177c4683052004"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": "",
   "id": "6d92a84dfaf2bef1"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 2
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython2",
   "version": "2.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
